在 FP 中,會發現我們其實沒有那麼常使用 for
、forEach
,更多時候是使用 filter
與 map
陣列方法,因為在 FP 這個設計模式中,為了讓程式碼的產出可以符合預期,我們會竟量避免使用可能會有副作用或是讓資料污染的狀況。
除了 map
與 filter
外,在 FP 中我們也很常使用 reduce
來進行資料的處理,在聊聊 reduce
在 FP 的應用前,讓我們先來簡單了解 reduce
的使用方法吧!
reduce()
是一個可以遍歷陣列的方法,通常作為累加器使用。
我們會在 reduce()
方法中帶入一個 callback 函式(這個函式稱為 reducer),陣列中的元素會在遍歷的過程,一一執行這個 reducer 函式,reducer 函式的第一個參數會是作為 reduce()
函式第二個參數的初始值,而 reducer 的第二個參數會是遍歷當前的元素:
const array1 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
(previousValue, currentValue) => previousValue + currentValue,
initialValue
);
上方範例的 initialValue
會在 reduce()
遍歷第一筆元素時,作為初始值,也就是 reducer
函式第一個參數 previousValue
,reducer
函式會回傳一個計算完的值(在這邊的範例為 previousValue + currentValue
),在下一次的遍歷中,作為 previousValue
傳入reducer
函式。
由於 reduce()
陣列方法可以在第二個參數傳入初始值的關係,所以可以進行比 map()
與 filter()
更複雜的計算,雖然我們可以透過 reduce
進行更複雜的計算,但在 MDN 文件中,曾提及使用 reduce()
的壞處:
「 reduce()
這種類型的遞迴函式非常強大,但對 JavaScript 開發經驗比較少的人來說,有時候會比較費解一點。如果要讓程式碼更簡潔時,可能就要衡量程式碼簡潔度與可讀性哪個比較重要。」
但當然,我們可以透過語意化的命名,甚至是搭配抽象化的方式讓 reduce()
更好被理解。
基於易讀性的關係,也許接下來的範例不像我們平常所撰寫的程式碼所直覺,但只要習慣後,會有:「啊!原來如此!」的感覺,接著就會對程式碼要如何進行抽象化越來越有感覺。
接著就讓我們來看看 reduce
除了可以進行數字的累加外,還能解決什麼問題吧!
在 FP 設計模式中,我們時常會將資料作為最後的考量(Data Last),也就是說資料結構不應該影響函式的運作,此時我們就需要非常豐富資料處理的手段,例如我們可能會需要將多層級的陣列或是物件進行,層級的拆解,這樣的過程被稱為攤平( Flatten)。
舉例來說,我們可以透過 reduce()
來攤平陣列:
const flattened = [[0, 1], [2, 3], [4, 5]].reduce(
(previousValue, currentValue) => previousValue.concat(currentValue),
[],
);
// [0, 1, 2, 3, 4, 5]
我們在 reduce()
中傳入空陣列作為初始值,並透過 concat()
方法進行陣列的串接,透過這樣的運算就可以將陣列的資料結構給攤平了,透過攤平的手法,我們可以將原本無法被套用陣列方法的資料,轉化爲可被遍歷的資料結構。
當然,我們可以利用相同手法來重組物件的資料結構:
// 物件屬性篩選器
const filterProps = obj => list => {
const keys = Object.keys(obj);
const filterProps = list;
return keys.reduce((prev, curr) => {
if (filterProps.find(i => i === curr)) {
return { ...prev, [curr]: obj[curr] };
}
return prev;
}, {})
};
// test function
const list = ['a', 'b'];
const obj = { a: 1, b: 1, d: 1 };
const newObj = filterProps(obj)(list);
// -> { a: 1, b: 1};
我們可以透過在 reduce()
傳入空物件的方式,進行我們想要資料重組的運算,舉例來說,我們只想要保留物件屬性中的特定幾個屬性,我們只要負責將想要重組的物件、想要保留的屬性列表傳入 filterProps
函式,就可以獲得我們想要重組的物件。
在上方的範例中,我們甚至將 filterProps
進行了科里化,這樣我就可以透過抽象化中特殊化的手法,重複利用指定的局部性應用函式。
當然,這些範例都可以再被優化或是更簡潔,就如同上方所提及,在使用 reduce()
時可讀性是非常重要的。
上面的範例中,我們大致上知道我們可以透過使用 reduce()
把資料結構進行排列組合達到不一樣的效果,但其實我們還可以透過 reduce()
搭配多組函式,替我們的函式進行自動化,也就是所謂管線(Pipe)的概念。
舉例來說,我們在實務開發中可能會遇到需要進行連續性的資料處理,拿到 A 資料後,進行 B 處理,再拿 B 處理完的資料進行 C 處理,此時我們就可以將要處理的資料作為初始值傳入 ,再透過逐步執行指定的函式後回傳結果,回傳的結果將做完下一次函式的初始值:
// 可以自動化同步函式的 pipe 函式
const pipe = init => funcs => funcs.reduce(((x, func) => func(x) ), init);
如果我們不透過 reduce()
來進行連續性的處理的話,我們的程式碼可能就會像是:
const a = 1;
const b = fn1(a);
const c = fn2(b);
但其實我們完全可以把上述的流程所會用到的函式,整理成有順序的陣列,再把這個整理好的陣列,與一開始的初始資料交給 pipe()
函式處理。
這也是為什麼每當人們提到 FP ,就會聯想到 reduce()
的原因,因為它能做到的事真的太多了!
講到這邊,可能會覺得:「如果我不熟 FP 該怎麼辦呢?」
畢竟對於開發經驗不多的人來說,要將這些看似簡單,但實際上有很多學問的函式進行重複組合,其實並不好上手,所以在下一個章節,我們要來介紹一些第三方函式庫,來幫助我們在寫程式的過程中不僅有範本可以參考,更能透過前人之手幫我們來解決更複雜的計算任務。
那我們就下一個章節見吧!